延續昨天介紹的 SSG,今天要介紹的是 SSG 的優化版本 Incremental Static Generation/Regeneration(ISR)。
為何會有 ISR 的出現?如同前篇所說,因為 SSG 需要預先渲染好所有路由需要的 HTML 檔案,當 HTML 變多時就會難以維護,且要更新內容時也需要全部重新建構,耗費成本較高,因此才出現 Incremental Static Generation(或稱 Regeneration,因為過程中有產生也有重新產生),此渲染模式中文稱作增量靜態產生/重新產生,也有人稱它為 iSSG(以下文章會以 ISR 稱呼)。
ISR 可算是一種 SSG 和 SSR 的混合體,它只預渲染某些靜態頁面,在使用者請求時,再依照需求渲染動態頁面,因此不會一開始就預渲染好全部路由的頁面,避免 SSG 一次預渲染大量頁面所耗費的成本,也節省修正或新增頁面內容就要全部重新 build 的時間與心力。
ISR 以 2 種方式,在現有靜態網站建構後逐步導入更新:
運用 lazy loading 的概念,新頁面在第一次請求時才立即產生,產生過程中會先向使用者顯示 fallback 或 loading 的提示 UI,和之前的 SSG 相比。SSG 如果請求不存在的路由頁面,會直接顯示 404 的 fallback 而不是顯示 loading fallback。
Next.js 範例:
// pages/articles/[id].js
// 1. 在 getStaticPaths() 中,回傳希望 build 階段預先渲染的 id 列表,可先從資料庫取得所有文章,並生成一個包含所有文章 ID 的 paths 陣列
export async function getStaticPaths() {
const articles = await getArticlesFromDatabase();
const paths = articles.map((article) => ({
params: { id: article.id }
}));
// fallback: true 代表找不到該 id 路徑的頁面時,不會直接顯示 404,而是顯示 fallback 頁面
// 這點和前面一般的 SSG 設定的 fallback false 不同
return { paths, fallback: true };
}
// 2. params 會包含生成文章頁需要的 id
export async function getStaticProps({ params }) {
return {
props: {
article: await getArticleFromDatabase(params.id)
}
}
}
export default function Article({ article }) {
const router = useRouter();
if (router.isFallback) {
return <div>Loading...</div>;
}
// 渲染文章頁面
}
使用者在請求頁面時,體驗上是差不多的,只有第一個請求還沒預渲染新頁面的使用者體驗會比較差(需要等待渲染好),後續請求的使用者都可以拿到預渲染好的頁面。預渲染和第一次請求時才渲染的示意圖如下。
圖 1 預渲染好的頁面直接回傳(資料來源:自行繪製)
圖 2 請求時立即產生該頁面(資料來源:自行繪製)
為現有頁面定義失效時間,只要超過失效時間,頁面就會重新驗證、重新產生新頁面。
此處使用的是 stale-while revalidate 策略,意思是在重新驗證頁面期間,使用者會先收到快取或舊的版本,重新驗證(重新產生)好以後,使用者下次請求就會收到新版本頁面,且重新驗證完全在背景執行,不需完全重建(rebuild)。
Next.js 範例:
// pages/articles/[id].js
// 1. getStaticProps 會在 build 階段執行
export async function getStaticProps() {
return {
props: {
articles: await getArticlesFromDatabase(),
revalidate: 60, // 回傳的 props 中,透過 revalidate 來強制頁面在 60 秒後重新驗證
}
}
}
// 2. page 元件會收到 getStaticProps 在 build 階段回傳的 articles prop
export default function Articles({ articles }) {
return (
<>
<h1>Articles</h1>
<ul>
{articles.map((article) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
</>
)
}
流程示意圖如下。
圖 3 更新現有頁面流程示意圖(資料來源:自行繪製)
補充:stale-while-revalidate
源自於 HTTP Cache-Control header 的一個屬性。
主要概念為:當第一次發出 request 時,瀏覽器會將回傳的資料存到快取裡,當之後又有相同的 request 時,瀏覽器會優先返回快取的版本,讓使用者可以迅速得到資料或是看到畫面,優化了使用者的體驗,並在 background 驗證快取的資料是不是已經過期,如果需要更新就會抓取最新資料並更新快取,當下次又有請求時就可以拿到剛剛更新過的、存到快取的資料。
屬於 ISR 的一種變體,只會在某些特定事件發生時才重新產生頁面,而不是固定時間間隔重新產生。
常規(一般) ISR 的更新後頁面只會在已處理使用者頁面請求的邊緣節點被快取,而按需求 ISR 則是透過邊緣網路重新產生、分發頁面,全球使用者都可自動從邊緣快取看到頁面的最新版本。
按需求 ISR 和一般 ISR 相比,可避免不必要的重新產生和 serveless function 的呼叫,能降低營運成本,並提供更好的效能和開發者體驗(DX)。
而按需求 ISR 缺點則是靜態渲染大都會有的缺點,它不適合高度動態、具個人化資料的頁面。
這幾天介紹了 CSR、SSR、SSG 和 ISR 的渲染方式,實作上 Next.js 都有方法可以實現,因此使用 Next.js 開發時,可依據需求來為不同頁面選擇適合的渲染方式,以下附上四種渲染方式的流程比較:
圖 4 Next.js 支援四種渲染方式(資料來源:https://guydumais.digital/blog/next-js-the-ultimate-cheat-sheet-to-page-rendering/)
也附上圖表統整不同渲染模式的特色與使用案例。
(圖中的 progressive hydration 可想成是 selective hydration 的前身)
圖 5 不同渲染模式的特色比較(資料來源:https://www.patterns.dev/vanilla/rendering-patterns)
這幾個渲染方式中,並沒有一個十全十美、效能最優秀的方案可套用於所有情境,該選擇哪種渲染方式需考量應用程式與頁面類型、內容而定,選擇了某方案享有它的優點的同時,也會需要犧牲、包容該方案的某些缺點,需看開發者如何取捨這之中的平衡,就像在 Day 25 渲染模式初探那篇文章所說,每個模式都是為解決特定案例而設計,適合的渲染模式可以為產品展現更出色的效能與表現,不適合的渲染模式則會為充滿創意的應用程式帶來負面影響。
另外補充,在 Patterns for Building JavaScript Websites in 2022 這篇文章中,作者提供了另一種角度,以常見的前端應用來分析其特徵並提出可能適合的渲染策略,對於不知如何選擇渲染模式的開發者來說,也可作為參考。
表 1 不同前端應用的特徵與適合的渲染策略(資料來源:https://dev.to/this-is-learning/patterns-for-building-javascript-websites-in-2022-5a93)
Portfolio | Content | Storefront | Social Network | Immersive | |
---|---|---|---|---|---|
Holotype | Personal Blog | CNN | Amazon | Figma | |
Interactivity | Minimal | Linked Articles | Purchase | Multi-Point, Real-time | Everything |
Session Depth | Shallow | Shallow | Shallow - Medium | Extended | Deep |
Values | Simplicity | Discover-ability | Load Performance | Dynamicism | Immersiveness |
Routing | Server | Server, Hybrid | Hybrid, Transitional | Transitional, Client | Client |
Rendering | Static | Static, SSR | Static, SSR | SSR | CSR |
Hydration | None | Progressive, Partial | Partial, Resumable | Any | None (CSR) |
Example Framework | 11ty | Astro, Elder | Marko, Qwik, Hydrogen | Next, Remix | Create React App** |
** Create React App:React 官方文件中已不再提及 Create React App,如果是作為單純練習用還可使用 Create React App,如果想開發 React CSR 的應用,個人建議可改用 Vite。